{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:45:08.299678Z",
     "start_time": "2025-02-26T09:45:05.181064Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 2.0.2\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.6.0+cu126\n",
      "cuda:0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:24.784980Z",
     "start_time": "2025-02-26T09:46:24.775027Z"
    }
   },
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing(data_home='data')\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:26.973336Z",
     "start_time": "2025-02-26T09:46:26.969335Z"
    }
   },
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:31.190737Z",
     "start_time": "2025-02-26T09:46:31.151008Z"
    }
   },
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "execution_count": 5
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:31.751082Z",
     "start_time": "2025-02-26T09:46:31.743146Z"
    }
   },
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 6
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集\n",
    "\n",
    "这里我们构建多输入的数据集，注意到数据集介绍里对每个特征定义如下：\n",
    "\n",
    "> **Attribute Information**:\n",
    "> - MedInc        median income in block group\n",
    "> - HouseAge      median house age in block group\n",
    "> - AveRooms      average number of rooms per household\n",
    "> - AveBedrms     average number of bedrooms per household\n",
    "> - Population    block group population\n",
    "> - AveOccup      average number of household members\n",
    "> - Latitude      block group latitude\n",
    "> - Longitude     block group longitude\n",
    "\n",
    "我们认为最后两维作为位置信息要单独处理，故制作数据集如下"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:33.146380Z",
     "start_time": "2025-02-26T09:46:33.137861Z"
    }
   },
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return (self.x[idx],self.x[idx][-2:]), self.y[idx] #返回的是一个元组，元组里面是两个元素，第一个元素是一个元组，第二个元素是一个数。self.x[idx][-2:]代表取最后两个元素\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "outputs": [],
   "execution_count": 7
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:36.221282Z",
     "start_time": "2025-02-26T09:46:36.214712Z"
    }
   },
   "source": [
    "train_ds[1]"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       "  tensor([ 1.0806, -1.0611])),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 8
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:46:40.593321Z",
     "start_time": "2025-02-26T09:46:40.590312Z"
    }
   },
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=False)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 9
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:48:31.649691Z",
     "start_time": "2025-02-26T09:48:31.644691Z"
    }
   },
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=(8,2)):\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim[1], 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim[0], 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x_wide, x_deep):\n",
    "        # x_deep.shape [batch size, 6]\n",
    "        deep_output = self.deep(x_deep)\n",
    "        # concat [batch size, 30] with [batch size 8]\n",
    "        concat = torch.cat([x_wide, deep_output], dim=1)\n",
    "        logits = self.output_layer(concat)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ],
   "outputs": [],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:50:49.901387Z",
     "start_time": "2025-02-26T09:50:49.897327Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [
    "model = WideDeep()\n",
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-26T09:50:50.998469Z",
     "start_time": "2025-02-26T09:50:50.994108Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "WideDeep(\n",
       "  (deep): Sequential(\n",
       "    (0): Linear(in_features=2, out_features=30, bias=True)\n",
       "    (1): ReLU()\n",
       "    (2): Linear(in_features=30, out_features=30, bias=True)\n",
       "    (3): ReLU()\n",
       "  )\n",
       "  (output_layer): Linear(in_features=38, out_features=1, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "source": [
    "for name, param in model.named_parameters():\n",
    "      print(name, param.shape)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-02-26T09:50:51.560960Z",
     "start_time": "2025-02-26T09:50:51.556959Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "deep.0.weight torch.Size([30, 2])\n",
      "deep.0.bias torch.Size([30])\n",
      "deep.2.weight torch.Size([30, 30])\n",
      "deep.2.bias torch.Size([30])\n",
      "output_layer.weight torch.Size([1, 38])\n",
      "output_layer.bias torch.Size([1])\n"
     ]
    }
   ],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:50:53.475556Z",
     "start_time": "2025-02-26T09:50:53.472314Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for (datas_deep, datas_wide), labels in dataloader:\n",
    "        datas_deep = datas_deep.to(device)\n",
    "        datas_wide = datas_wide.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas_deep, datas_wide)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ],
   "outputs": [],
   "execution_count": 14
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:51:45.980791Z",
     "start_time": "2025-02-26T09:51:11.551974Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for (datas_deep, datas_wide), labels in train_loader:#和上面自定义的dataset是一致的\n",
    "                datas_deep = datas_deep.to(device)\n",
    "                datas_wide = datas_wide.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas_deep, datas_wide)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/14520 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "594e775fed3c48048be35b89c3ad9cfc"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 15
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:52:22.783402Z",
     "start_time": "2025-02-26T09:52:22.689079Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "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Gz4PPpKTkxEREYEjR44Uul2u16xZ86zto6Oj1akoeUH99aL687GDGdttLEZtt5HbznYbiyXI2l2effF5wWlUVBQ6d+6MWbNmuW+z2+3qumc3DBERERmTX7pdpBtl6NCh6NKli5rbQ4baZmZmqtEvREREZGx+CT5uvPFGHDt2DM8//7yaZKxDhw6YPn06atQw3jTbREREpFPB6YMPPqhORERERJ6Ca7k9IiIiCnsMPoiIiEhXDD6IiIhIVww+iIiISFcMPoiIiEhXAZ9evSjXUjPlmaa1PFPSytzz8tjBNCucv7HdbLdRGLXtbDfbHQxcx21vlowLuuDj9OnT6lzWdyEiIqLQIsfxSpUq6buq7bmSqdhlZdyEhASYTCafPrZr0bp9+/b5fNG6YMZ2s91GYdS2s91sdzCQcEICj9q1a8NsNodW5kN2uG7dun59DvljBdMfTC9st7EYtd1GbjvbbSyJQdjusjIeLiw4JSIiIl0x+CAiIiJdGSr4iI6OxgsvvKDOjYTtZruNwqhtZ7vZ7lATdAWnREREFN4MlfkgIiKiwGPwQURERLpi8EFERES6YvBBREREujJM8PHee++hYcOGiImJQbdu3bB8+XKEkrFjx6Jr165q5tfq1avj6quvxpYtWwptk5OTg+HDh6Nq1aqIj4/HddddhyNHjhTaZu/evRg0aBCsVqt6nCeffBL5+fmFtpk7dy46deqkKqmbNGmCiRMnIhi8+uqratbbRx55xBBtPnDgAG699VbVttjYWLRt2xYrV6503y+14s8//zxq1aql7u/bty+2bdtW6DFSU1MxZMgQNRFR5cqVcddddyEjI6PQNuvWrcOFF16oPhsya+K4ceMQKAUFBXjuueeQkpKi2tS4cWOMGTOm0FoR4dDu+fPn44orrlAzQcp7+ueffy50v55t/P7779GiRQu1jbzHpk6dikC1XdYsGTFihNqPuLg4tc3tt9+uZr0O9baX9Tf3dN9996ltxo8fH/LtLpFmAJMnT9aioqK0zz77TNuwYYN29913a5UrV9aOHDmihYoBAwZon3/+ufbPP/9oa9as0S677DKtfv36WkZGhnub++67T6tXr542a9YsbeXKldr555+vde/e3X1/fn6+1qZNG61v377a6tWrtalTp2rJycnayJEj3dvs3LlTs1qt2mOPPaZt3LhRmzBhghYREaFNnz5dC6Tly5drDRs21Nq1a6c9/PDDYd/m1NRUrUGDBtodd9yhLVu2TO3jn3/+qW3fvt29zauvvqpVqlRJ+/nnn7W1a9dqV155pZaSkqJlZ2e7t7n00ku19u3ba0uXLtUWLFigNWnSRLv55pvd96elpWk1atTQhgwZot5b33zzjRYbG6t99NFHWiC8/PLLWtWqVbXff/9d27Vrl/b9999r8fHx2jvvvBNW7Zb34TPPPKP99NNPElVpU6ZMKXS/Xm1ctGiReq+PGzdOvfefffZZzWKxaOvXrw9I20+dOqU+q99++622efNmbcmSJdp5552nde7cudBjhGLby/qbu8j90rbatWtrb7/9thbq7S6JIYIPefMOHz7cfb2goED9YceOHauFqqNHj6o38Lx589wfWnkDyZe1y6ZNm9Q28gF2vfnNZrN2+PBh9zYffPCBlpiYqOXm5qrrTz31lNa6detCz3XjjTeq4CdQTp8+rTVt2lSbOXOmdtFFF7mDj3Bu84gRI7SePXuWeL/dbtdq1qypvf766+7b5PWIjo5WXzhCvljktVixYoV7m2nTpmkmk0k7cOCAuv7+++9rVapUcb8Wrudu3ry5FgiDBg3S7rzzzkK3XXvtterLNFzbXfRApGcbb7jhBvWae+rWrZt27733anoo7SDs+cNDttuzZ0/YtB0ltHv//v1anTp1VOAgPz48g49waLensO92ycvLw6pVq1Ta0nP9GLm+ZMkShKq0tDR1npSUpM6ljZKy9GynpNXq16/vbqecS4qtRo0a7m0GDBigFinasGGDexvPx3BtE8jXSrpVpNuk6H6Fc5t//fVXdOnSBYMHD1ZdRR07dsQnn3zivn/Xrl04fPhwof2WNRWkS9Gz7ZKalcdxke3l/b9s2TL3Nr169UJUVFShtkuX3smTJ6G37t27Y9asWdi6dau6vnbtWixcuBADBw4M63Z70rONwfjeL+67TrogpL3h3Ha73Y7bbrtNdQu3bt36rPvDrd1hH3wcP35c9SN7HnyEXJcPeCiSN6nUPfTo0QNt2rRRt0lb5A3n+oAW1045L+51cN1X2jZysM7OzobeJk+ejL///lvVvBQVrm0WO3fuxAcffICmTZvizz//xP3334+HHnoIX3zxRaF9L+19LecSuHiKjIxUAWt5Xh89Pf3007jppptUEGmxWFTQJe916ecO53Z7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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 16
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:52:29.292981Z",
     "start_time": "2025-02-26T09:52:28.908820Z"
    }
   },
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.5162\n"
     ]
    }
   ],
   "execution_count": 17
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": ""
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
